import AbstractGesture from './AbstractGesture';
import Point from '../geometry/Point';
import { bindAll } from '../utils/utils';
import Camera from '../camera/Camera';
import Animator from '../animate/Animator';
import { getPitchInertia, getRotateInertia } from './InertiaUtils';

import Panorama from '../view/Panorama';
import { CameraOptions } from '../typings/PanoramaType';

class DragRotate extends AbstractGesture<[number, number, number]> {
    private _curPos?: Point;
    private _lastPos?: Point;
    private _rotateEnable: boolean = true;
    private _pitchEnable: boolean = true;

    constructor(view: Panorama) {
        super(view);
        bindAll([
            '_frameUpdate',
        ], this);
    }

    enableRotate() {
        this._rotateEnable = true;
    }

    disableRotate() {
        this._rotateEnable = false;
    }

    enablePitch() {
        this._pitchEnable = true;
    }

    disablePitch() {
        this._pitchEnable = false;
    }

    private _isEnable() {
        return this._rotateEnable || this._pitchEnable;
    }

    onMousedown(e: MouseEvent) {
        if (e.buttons !== 1 || !this._isEnable()) return;
        this._start(e);
    }

    onMousemove(e: MouseEvent) {
        if (e.buttons !== 1 || !this._isEnable() || !this._isCanMove) return;
        this._onMove({ x: e.clientX, y: e.clientY }, e);
    }

    onMouseup(e: MouseEvent) {
        this._stopFrameUpdate(e);
    }

    onMouseleave(e: MouseEvent) {
        this._stopFrameUpdate(e);
    }

    onTouchstart(e: TouchEvent) {
        if (!this._isEnable()) return;
        this._start(e.touches[0]);
    }

    onTouchmove(e: TouchEvent) {
        if (!this._isEnable() || !this._isCanMove) return;
        const touch = e.touches[0];
        this._onMove({ x: touch.clientX, y: touch.clientY }, e);
    }

    onTouchend(e: TouchEvent) {
        this._stopFrameUpdate(e);
    }

    private _start(e: Touch | MouseEvent) {
        this._state = 'pending';
        this._isCanMove = true;
        this._lastPos = new Point(e.clientX, e.clientY);
        this._inertias = [[Date.now(), <number>this._view.getBearing(), <number>this._view.getPitch()]];
        if (this.animator) {
            this.animator.stop();
            this.animator = undefined;
        }
    }

    private _onMove(point: Coordinate, e: MouseEvent | TouchEvent) {
        if (this._state === 'pending') {
            this._state = 'active';
            this._startFrameUpdate();
            this._view.fire('moveStart', e);
            this._rotateEnable && this._view.fire('bearingStart', e);
            this._pitchEnable && this._view.fire('pitchStart', e);
            this._view.fire('gestureStart');
        }
        this._curPos = new Point(point.x, point.y);
    }

    protected _frameUpdate() {
        const engine = this._view.getEngine();
        if (engine && this._lastPos && this._curPos) {
            const camera = engine.getCamera();
            const angle = this._curPos.x - this._lastPos.x;
            let bearing = camera.getBearing();
            bearing -= angle / 15;
            let pitch = camera.getPitch();
            const deltaY = this._curPos.y - this._lastPos.y;
            pitch += deltaY / 10;
            const params: CameraOptions = {};
            if (this._pitchEnable) {
                params.pitch = pitch;
            }
            if (this._rotateEnable) {
                params.bearing = bearing;
                const last = this._inertias[this._inertias.length - 1];
                this._inertias.push([Date.now(), Camera.normalizeBearing(bearing, last[1]), pitch]);
            }

            camera.set(params);
            this._lastPos = this._curPos;
            engine.render();
        }
        this._view.fire('move');
        this._rotateEnable && this._view.fire('bearing');
        this._pitchEnable && this._view.fire('pitch');
        this._view.fire('gesture');
        this._timer = requestAnimationFrame(this._frameUpdate);
    }

    protected _stopFrameUpdate(e: MouseEvent | TouchEvent) {
        super._stopFrameUpdate(e);
        delete this._lastPos;
        delete this._curPos;
    }

    protected _inertialEnd(e: MouseEvent | TouchEvent) {
        this._initInertial();

        if (this._inertias.length < 2) {
            this._fireEnd(e);
            return;
        }
        const start = this._inertias[0];
        const end = this._inertias[this._inertias.length - 1];
        const flingDuration = (end[0] - start[0]) / 1000;
        const rotateInertia = getRotateInertia(end[1] - start[1], flingDuration);
        const pitchInertia = getPitchInertia(end[2] - start[2], flingDuration);
        if (!rotateInertia && !pitchInertia) {
            this._fireEnd(e);
            return;
        }
        let pitchOffset = 0, rotateOffset = 0, duration = 0;
        if (rotateInertia && pitchInertia) {
            duration = Math.max(pitchInertia.duration, rotateInertia.duration);
            pitchOffset = pitchInertia.offset;
            rotateOffset = rotateInertia.offset;
        } else if (pitchInertia) {
            duration = pitchInertia.duration;
            pitchOffset = pitchInertia.offset;
        } else if (rotateInertia) {
            duration = rotateInertia.duration;
            rotateOffset = rotateInertia.offset;
        }
        this._rotateInertia(rotateOffset, pitchOffset, duration * 1000, e);
        this._inertias = [];
    }

    private _rotateInertia(rotateOffset: number, pitchOffset: number, duration: number, event: MouseEvent | TouchEvent) {
        const engine = this._view.getEngine();
        if (!engine) return;
        const camera = engine.getCamera();
        const startBearing = camera.getBearing();
        const startPitch = camera.getPitch();
        const animator = new Animator().ofNumber(0, 1, duration);
        this.animator = animator;
        animator.easing('easeOutCirc');
        animator.on('update', (e) => {
            const curBearing = rotateOffset * e.num + startBearing;
            const curPitch = pitchOffset * e.num + startPitch;
            camera.set({ bearing: curBearing, pitch: curPitch });
            engine.render();
            this._view.fire('move', event);
            this._view.fire('bearing', event);
        }).on('complete', () => {
            this._fireEnd(event);
            this.animator = undefined;
        }).on('stop', () => {
            this._fireEnd(event);
        }).start();
    }

    private _fireEnd(e: MouseEvent | TouchEvent) {
        this._view.fire('moveEnd', e);
        this._rotateEnable && this._view.fire('bearingEnd', e);
        this._pitchEnable && this._view.fire('pitchEnd', e);
        this._view.fire('gestureEnd', e);
    }
}

export default DragRotate;
